Like any kind of apps, JavaScript apps also have to be written well.
Otherwise, we run into all kinds of issues later on.
In this article, we’ll look at some best practices we should follow when writing Node apps.
Folder Structure
Our Node app should follow some standard folder structure.
For example, we can have something like:
src
│ app.js
└───api
└───config
└───jobs
└───loaders
└───models
└───services
└───subscribers
└───types
app.js
is the app’s entry point.
api
has the controllers for the endpoints.
config
has the environment variables and configuration related stuff.
jobs
have scheduled jobs.
loaders
have the code that runs when the app starts.
models
have the database models.
services
has business logic.
subscribers
have the event handlers for queues, etc.
types
are type definitions for TypeScript projects.
3 Layer Architecture
The 3 layer architecture consists of the controllers, service layer, and the data access layer.
The controller is the interface to the client.
It takes requests and sends responses.
The service layer has the logic, which takes stuff from controllers and returns things to them.
The data access layer has the database logic which talks to the service layer.
We should never put business logic in controllers.
Separating them makes testing them easier when we write unit tests.
When we test controllers, we can just mock all the service entities.
Service Layer for Business Logic
The service layer is used for business logic.
It’s all about doing everything that controllers and database layers don’t do.
For example, we can write:
route.post('/',
validators.userSignup,
async (req, res, next) => {
const userParams = req.body;
const { user } = await UserService.Signup(userParams);
return res.json(user);
});
The UserService.Signup
has the business logic.
The request and response are handled by the controller.
Pub/Sub Layer
The pub/sub layer is used for listening to events from external sources.
The pub part sends data to other modules.
Separating this logic makes sense since they create a cohesive layer.
Dependency Injection
Dependency injection lets us handle all the dependency initialization in one place.
For example, we can write:
export default class UserService {
constructor(userModel, companyModel, employeeModel){
this.userModel = userModel;
this.companyModel = companyModel;
this.employeeModel = employeeModel;
}
getUser(userId){
const user = this.userModel.findById(userId);
return user;
}
}
We have the UserService
class that has takes all the required dependencies in the constructor.
Then we use throughout the class.
We can use 3rd party solutions to make this easier.
The typedi library provides us with a dependency injection container to let us inject dependencies.
We can use dependency injection with Express by writing something like:
route.post('/',
async (req, res, next) => {
const userParams = req.body;
const userServiceInstance = Container.get(UserService);
const { user} = userServiceInstance.Signup(userParams);
return res.json(user);
});
We call the Container.get
method from typedi
to get the UserService
so we can use it.
Cron Jobs and Recurring Task
Cron jobs and scheduled tasks should be in their own folder.
We can use the business logic from the service layer.
Also, we shouldn’t use setTimeout
or another primitive way to delay the execution of code.
Instead, we should use a library to help us with this.
This way, we can control failed jobs and have feedback on ones that succeed.
Conclusion
We should create apps with a standard structure.
The folders cohesively organize different parts of our app.